--[[
    SpamBayes for Eso, a lightweight bayesian antispam for The Elder Scrolls Online
    Copyright @ 2010 Arnaud Dovi (@class101 at EU-MegaServer, ad@heapoverflow.com)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
]]

--[[
$Rev:: 9                     $:  Revision of last commit
$Author:: ad@heapoverflow.co#$:  Author of last commit
$Date:: 2014-04-29 10:56:55 #$:  Date of last commit
$Eso:: 1.0.5                 $:  Client of the last tests
]]

----------------- GLOBALS ----------------

------------------------------------------
local db, xml, origOnLinkClicked, origCreateLink, origOnChatEvent
local SpamBayes                         = SpamBayes
local getprob                           = SpamBayes.chi2.getprob
local firstLoad                         = true
local isEventRegistered                 = false
local trainTable                        = {}
local lineId                            = 0

local type                              = type
local tonumber                          = tonumber
local tostring                          = tostring
local table_concat                      = table.concat

local ZO_ChatWindow                     = ZO_ChatWindow
local GetDisplayName, ZO_Menu, ShowMenu = GetDisplayName, ZO_Menu, ShowMenu
local CHARACTER_LINK_TYPE               = CHARACTER_LINK_TYPE
local DISPLAY_NAME_LINK_TYPE            = DISPLAY_NAME_LINK_TYPE
local GetTimeString, AddMenuItem        = GetTimeString, AddMenuItem
local zo_strformat                      = zo_strformat
local IsChatContainerTabCategoryEnabled = IsChatContainerTabCategoryEnabled
local EVENT_CHAT_MESSAGE_CHANNEL        = EVENT_CHAT_MESSAGE_CHANNEL

local MINOR_VERSION                     = tonumber(("$Rev: 9 $"):match("%d+"))
SpamBayes.MINOR_VERSION                 = SpamBayes.MINOR_VERSION == nil and MINOR_VERSION or (MINOR_VERSION > SpamBayes.MINOR_VERSION and MINOR_VERSION or SpamBayes.MINOR_VERSION)

-- 90% or greater   = SPAM (unwanted)
-- 20% or lower     = HAM (wanted)
-- anything between = UNSURE
local ME
local SPAM_CUTOFF                       = 0.90
local HAM_CUTOFF                        = 0.20
local COLOR_SPAM                        = "|cff0000"
local COLOR_HAM                         = "|c00ff00"
local COLOR_UNSURE                      = "|cffffff"
local COLOR_UNTRAIN                     = "|c808080"
local COLOR_GOLD                        = "|ceeee00"
local COLOR_STOP                        = "|r"
local LINEID_TAG                        = "sbid_"

local hardcoded = { -- removing a line in this table also disables the chat message type from being scanned
    --[CHAT_CHANNEL_GUILD_2]              = {SI_CHAT_MESSAGE_GUILD,        CHAT_CATEGORY_GUILD_2},      --  test channel
    [CHAT_CHANNEL_ZONE]                 = {SI_CHAT_MESSAGE_ZONE,         CHAT_CATEGORY_ZONE},         --  6 SI_CHAT_MESSAGE_ZONE         : "[eng] <<1>> zone: <<X:2>>"
    [CHAT_CHANNEL_SAY]                  = {SI_CHAT_MESSAGE_SAY,          CHAT_CATEGORY_SAY},          --  1 SI_CHAT_MESSAGE_SAY          : "[eng] <<1>> says: <<X:2>>"
    [CHAT_CHANNEL_YELL]                 = {SI_CHAT_MESSAGE_YELL,         CHAT_CATEGORY_YELL},         --  2 SI_CHAT_MESSAGE_YELL         : "[eng] <<1>> yells: <<X:2>>"
    [CHAT_CHANNEL_ZONE_LANGUAGE_1]      = {SI_CHAT_MESSAGE_ZONE_ENGLISH, CHAT_CATEGORY_ZONE_ENGLISH}, -- 20 SI_CHAT_MESSAGE_ZONE_ENGLISH : "[eng] <<1>> zone - English: <<X:2>>"
    [CHAT_CHANNEL_ZONE_LANGUAGE_2]      = {SI_CHAT_MESSAGE_ZONE_FRENCH,  CHAT_CATEGORY_ZONE_FRENCH},  -- 21 SI_CHAT_MESSAGE_ZONE_FRENCH  : "[eng] <<1>> zone - French: <<X:2>>"
    [CHAT_CHANNEL_ZONE_LANGUAGE_3]      = {SI_CHAT_MESSAGE_ZONE_GERMAN,  CHAT_CATEGORY_ZONE_GERMAN}   -- 22 SI_CHAT_MESSAGE_ZONE_GERMAN  : "[eng] <<1>> zone - German: <<X:2>>"
}

local links_type = { -- Color spammers now uses |H color links, need to check link validity
    [0] = "|H[A-F0-9][A-F0-9][A-F0-9][A-F0-9][A-F0-9][A-F0-9]:item:%d+:%d+:%d+:%d+:%d+:%d+:%d+:%d+:%d+:%d+:%d+:%d+:%d+:%d+:%d+:%d+:%d+:%d+:%d+:%d+|h%[[^%]]+%]|h", -- ITEM_LINK_TYPE        |H2DC50E:item:30164:1:7:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0|h[Ancolie]|h
    [1] = "|H[A-F0-9][A-F0-9][A-F0-9][A-F0-9][A-F0-9][A-F0-9]:achievement:%d+:%d+:%d+|h%[[^%]]+%]|h",                                                              -- ACHIEVEMENT_LINK_TYPE |HDCD822:achievement:554:65535:1397743073|h[Chasseur d'éclats célestes de Fendretour]|h
    [2] = "|H[A-F0-9][A-F0-9][A-F0-9][A-F0-9][A-F0-9][A-F0-9]:book:%d+|h%[[^%]]+%]|h"                                                                              -- BOOK_LINK_TYPE        |HE27A3F:book:156|h[Jorunn le Roi-scalde]|h
}

local function D(text)
    d("|c808080[SpamBayes] : "..text.."|r")
end

local function has_invalid_link(text)
    if text:match("|[hH]") then
        for a in text:gmatch("|H.-:.-|h.-|h") do
            local isLinkValid = false
            for i = 0, #links_type do
                if a:match(links_type[i]) then
                    isLinkValid = true
                    break
                end
            end
            if not isLinkValid then
                return true
            end
        end
    end
    return false
end

local function filter_color_spam(text)
    if has_invalid_link(text) then
        text = text:gsub("|[hH][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]", ""):gsub("|[hH]", ""):gsub("|[rR]", "")
    end
    return text:gsub("|[cC][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]", ""):gsub("|[rR]", "") -- Fuck the color spammers !
end

local function strip_lineid(text)
    return text:gsub(LINEID_TAG.."%d+", "")
end

local function string_split(text, sep, pattern)
    local sep, fields = sep or " ", {}
    local patt = pattern or ("([^%s]+)"):format(sep)
    text:gsub(patt, function(c) fields[#fields+1] = c:lower() end)
    return fields
end

local function tokenize(msg)
    return string_split(tostring(msg), _, "([^%s%p]+)")
end

local function train(id, isSpam, unLearn)
    local tokens = trainTable[id] ~= nil and tokenize(trainTable[id]) or D("Unexpected error while "..(unLearn and "unlearning" or "training").." as "..(isSpam and "SPAM" or "HAM").." (please retry this is a known client bug")
    if type(tokens) == "table" then
        local success
        if unLearn then
            success = SpamBayes.chi2.unlearn(tokens, isSpam)
        else
            SpamBayes.chi2.learn(tokens, isSpam)
        end
        D((success == 0 and "Error with " or "Success with ")..(unLearn and "unlearning" or "training").." one more "..(isSpam and "SPAM" or "HAM")..(success == 0 and " (the phrase was never learned as "..(isSpam and "spam" or "ham").." or slightly changed)" or ""))
        if success == nil then
            xml.updateCounters(#tokens)
        else
            xml.updateCounters(nil, success ~= 0 and #tokens or nil)
        end
    end
end

local function IsChatFiltered(category)  -- esoui\ingame\chatsystem\chatsystem.lua, line 994
    if ZO_ChatWindow and ZO_ChatWindow.container and ZO_ChatWindow.container.windows then
        for i = 1, #ZO_ChatWindow.container.windows do
            if IsChatContainerTabCategoryEnabled(ZO_ChatWindow.container.id, i, category) then
                return true
            end
        end
    end
    return false
end

local function GetFormatString(messageType, msg, player, prob, isHam, isSpam, lineId)
    local sender    = ("|H%s:%s:%s:%s|h[%s]|h"):format("ignore", not player:find("@") and CHARACTER_LINK_TYPE or DISPLAY_NAME_LINK_TYPE, player, LINEID_TAG..lineId, player)
    local timestamp = "|c808080["..GetTimeString().."] "..COLOR_STOP --date("|cff808080["..(not db.time12 and "%H:%M" or "%I:%M %p").."]|c ")
    local prob_l    = ("%.1f"):format(prob * 100)
    local prob_t    = (prob_l:len() == 4 and "[%s %%] " or (prob_l:len() == 5 and prob_l == "100.0") and "[ 100 %%] " or "[0%s %%] "):format(prob_l)
    local color     = isHam and COLOR_HAM or (isSpam and COLOR_SPAM or COLOR_UNSURE)
    return timestamp..color..prob_t..zo_strformat(hardcoded[messageType][1], sender, msg)..COLOR_STOP
end

local function newCreateLink(text, color, type, ...)
    if (type == CHARACTER_LINK_TYPE or type == DISPLAY_NAME_LINK_TYPE) and text:find(LINEID_TAG) then
        local ntext = strip_lineid(text)
        local id = text:gsub(ntext, "")
        return ("|H%s:%s|h[%s]|h"):format(color and color:ToHex() or "ignore", table_concat({type, ntext, id}, ":"), ntext)
    end
    return origCreateLink(text, color, type, ...)
end

local function newOnLinkClicked(linkData, linkText, button, ...)
    origOnLinkClicked(linkData, linkText, button, ...)
    if button == 2 and (linkData):find(LINEID_TAG) then
        local id = tonumber(linkData:match(LINEID_TAG.."(%d+)"))
        ZO_Menu:SetHidden(true)
        if db.contextMenuShowPlayer then AddMenuItem("Char: "..COLOR_GOLD..linkText..COLOR_STOP, function() end) end
        AddMenuItem(COLOR_HAM.."Train as HAM"..COLOR_STOP, function() train(id, false) end)
        AddMenuItem(COLOR_SPAM.."Train as SPAM"..COLOR_STOP, function() train(id, true) end)
        AddMenuItem(COLOR_UNTRAIN.."Unlearn HAM"..COLOR_STOP, function() train(id, false, true) end)
        AddMenuItem(COLOR_UNTRAIN.."Unlearn SPAM"..COLOR_STOP, function() train(id, true, true) end)
        if db.clipboard then AddMenuItem("Copy to clipboard", function() xml.clipboardCopy(linkText.." "..trainTable[id]) end) end
        ShowMenu(nil, 1)
    end
end

local function filter(this, event, messageType, player, text, ...)
    local idTag = ""
    if event == EVENT_CHAT_MESSAGE_CHANNEL then
        --text = "W |HFF0000:hg|hW|h    W  . |HFF0000:cy|hM|h    |HFF0000:ur|hM|h O  U U    |HFF0000:ze|h.|h  C  |HFF0000:aq|hO|h M          100  |HFF0000:ev|hk|h    |HFFFF00:hh|h=|h 11.14  EUR"
        if db.filterColorSpammers then text = filter_color_spam(text) end
        if hardcoded[messageType] ~= nil and IsChatFiltered(hardcoded[messageType][2]) and player ~= ME then
            trainTable[lineId]           = text
            idTag                        = LINEID_TAG..lineId
            local prob                   = getprob(tokenize(text))
            local isHam                  = prob <= HAM_CUTOFF
            local isSpam                 = prob >= SPAM_CUTOFF
            xml.log(GetFormatString(messageType, text, player, prob, isHam, isSpam, lineId))
            lineId                       = lineId + 1
            if isHam then
                db.classifierdb.cham    = db.classifierdb.cham + 1
            elseif isSpam then
                db.classifierdb.cspam   = db.classifierdb.cspam + 1
                xml.updateCounters()
                return
            else
                db.classifierdb.cunsure = db.classifierdb.cunsure + 1
            end
            xml.updateCounters()
        end
    end
    if event == EVENT_FRIEND_PLAYER_STATUS_CHANGED and db.ignoreFriendsStatusChange then
        return
    end
    origOnChatEvent(this, event, messageType, (player and player..idTag) or player, text, ...) -- trick to later inject the lineId into the player's link, ugly but smallest and reliable way to achieve that
end

local function Install_or_Remove_Hooks(forceOff)
    if not origOnChatEvent then origOnChatEvent = CHAT_SYSTEM.OnChatEvent end
    if not origCreateLink then origCreateLink = ZO_LinkHandler_CreateLink end
    if not origOnLinkClicked then origOnLinkClicked = ZO_ChatSystem_OnLinkClicked end
    if not forceOff and db.isAddOnEnabled then
        if not isEventRegistered then
            isEventRegistered = true
            CHAT_SYSTEM.OnChatEvent = filter -- places the filter on top of everything we need
            ZO_LinkHandler_CreateLink = newCreateLink -- injects LINEID_TAG into the player's link of the ChatWindow
            ZO_ChatSystem_OnLinkClicked = newOnLinkClicked -- injects the train HAM/SPAM buttons into the player menu
        end
    else
        if isEventRegistered then
            isEventRegistered = false
            if origOnChatEvent then CHAT_SYSTEM.OnChatEvent = origOnChatEvent end
            if origCreateLink then ZO_LinkHandler_CreateLink = origCreateLink end
            if origOnLinkClicked then ZO_ChatSystem_OnLinkClicked = origOnLinkClicked end
        end
    end
end

--local function main(event, addon)
local function main()
    if firstLoad --[[ and addon == "SpamBayes"]] then
        firstLoad              = false
        SPAMBAYESDB            = SPAMBAYESDB              or { isAddOnEnabled = true, linksEnabled = false, autoreportEnabled = false , hiderpEnabled = false, isUpdate = false, filterColorSpammers = true, ignoreFriendsStatusChange = false, contextMenuShowPlayer = true, clipboard = true }
        db                     = SPAMBAYESDB
        db.worddb              = SPAMBAYESDB.worddb       or {}
        db.testdb              = SPAMBAYESDB.testdb       or {}
        db.classifierdb        = SPAMBAYESDB.classifierdb or { nham = 0, nspam = 0, cspam = 0, cham = 0, cunsure = 0, probcache = {}}
        xml = SpamBayes.xml
        local function toggle()
            if not xml.frame:IsHidden() then
                xml.aframe:SetHidden(true)
                xml.oframe:SetHidden(true)
                xml.hframe:SetHidden(true)
                return xml.frame:SetHidden(true)
            end
            return xml.frame:SetHidden(false)
        end
        SLASH_COMMANDS["/spambayes"] = toggle
        SLASH_COMMANDS["/sb"] = toggle
        SLASH_COMMANDS["/rl"] = ReloadUI
        SpamBayes.chi2.init(db)
        xml.init(db)
        xml.init = nil
        ME = GetUnitName("player")..(GetUnitGender("player") == GENDER_MALE and "^Mx" or "^Fx") -- ^Mx appends our own player name in ChatSystem:OnChatEvent
        Install_or_Remove_Hooks(false)
        D(db.isAddOnEnabled and "ON" or "OFF")
    end
end

SpamBayes.shutdown       = Install_or_Remove_Hooks

--EVENT_MANAGER:RegisterForEvent("SpamBayes", EVENT_ADD_ON_LOADED, main)
EVENT_MANAGER:RegisterForEvent("SpamBayes", EVENT_PLAYER_ACTIVATED, main)

--/script d(WINDOW_MANAGER:GetMouseOverControl():GetName())
--## DependsOn: ZO_Ingame
